using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using SharpCompress.Common.Tar.Headers;
using SharpCompress.Compressors;
using SharpCompress.Compressors.Deflate;

namespace SharpCompress.Common.GZip;

internal sealed class GZipFilePart : FilePart
{
    private string? _name;
    private readonly Stream _stream;

    internal GZipFilePart(Stream stream, ArchiveEncoding archiveEncoding)
        : base(archiveEncoding)
    {
        _stream = stream;
        ReadAndValidateGzipHeader();
        if (stream.CanSeek)
        {
            var position = stream.Position;
            stream.Position = stream.Length - 8;
            ReadTrailer();
            stream.Position = position;
        }
        EntryStartPosition = stream.Position;
    }

    internal long EntryStartPosition { get; }

    internal DateTime? DateModified { get; private set; }
    internal uint? Crc { get; private set; }
    internal uint? UncompressedSize { get; private set; }

    internal override string FilePartName => _name!;

    internal override Stream GetCompressedStream() =>
        new DeflateStream(_stream, CompressionMode.Decompress, CompressionLevel.Default);

    internal override Stream GetRawStream() => _stream;

    private void ReadTrailer()
    {
        // Read and potentially verify the GZIP trailer: CRC32 and  size mod 2^32
        Span<byte> trailer = stackalloc byte[8];
        var n = _stream.Read(trailer);

        Crc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
        UncompressedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.Slice(4));
    }

    private void ReadAndValidateGzipHeader()
    {
        // read the header on the first read
        Span<byte> header = stackalloc byte[10];
        var n = _stream.Read(header);

        // workitem 8501: handle edge case (decompress empty stream)
        if (n == 0)
        {
            return;
        }

        if (n != 10)
        {
            throw new ZlibException("Not a valid GZIP stream.");
        }

        if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
        {
            throw new ZlibException("Bad GZIP header.");
        }

        var timet = BinaryPrimitives.ReadInt32LittleEndian(header.Slice(4));
        DateModified = TarHeader.EPOCH.AddSeconds(timet);
        if ((header[3] & 0x04) == 0x04)
        {
            // read and discard extra field
            n = _stream.Read(header.Slice(0, 2)); // 2-byte length field

            var extraLength = (short)(header[0] + (header[1] * 256));
            var extra = new byte[extraLength];

            if (!_stream.ReadFully(extra))
            {
                throw new ZlibException("Unexpected end-of-file reading GZIP header.");
            }
            n = extraLength;
        }
        if ((header[3] & 0x08) == 0x08)
        {
            _name = ReadZeroTerminatedString(_stream);
        }
        if ((header[3] & 0x10) == 0x010)
        {
            ReadZeroTerminatedString(_stream);
        }
        if ((header[3] & 0x02) == 0x02)
        {
            _stream.ReadByte(); // CRC16, ignore
        }
    }

    private string ReadZeroTerminatedString(Stream stream)
    {
        Span<byte> buf1 = stackalloc byte[1];
        var list = new List<byte>();
        var done = false;
        do
        {
            // workitem 7740
            var n = stream.Read(buf1);
            if (n != 1)
            {
                throw new ZlibException("Unexpected EOF reading GZIP header.");
            }
            if (buf1[0] == 0)
            {
                done = true;
            }
            else
            {
                list.Add(buf1[0]);
            }
        } while (!done);
        var buffer = list.ToArray();
        return ArchiveEncoding.Decode(buffer);
    }
}
